'use strict';

var extend = require('extend');
var inherits = require('util').inherits;
var Validator = require('./validator/validator.js');

module.exports = RequestSanitizer;

function RequestSanitizer(options) {
	Validator.call(this, options);
}

inherits(RequestSanitizer, Validator);

RequestSanitizer.prototype.rules = {};
extend(RequestSanitizer, Validator);

RequestSanitizer.options = extend({
	reporter : SanitizeReporter
}, Validator.options);

RequestSanitizer.addRule('empty', {
	validate : function (accept, value, field) {
		if (accept !== false) { return; }

		return !field.isEmpty();
	}
});

RequestSanitizer.addRule('string', {
	filter : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

		return String(value||'');
	}
});

RequestSanitizer.addRule('regexp', {
	message : 'value should match template',
	validate : function (accept, value) {
		if (typeof value !== 'string') { return false; }

		return accept.test(value);
	}
});

RequestSanitizer.addRule('boolean', {
	filter : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

		switch(String(value).toLowerCase()) {
			case 'false':
			case 'f':
				return false;
			default:
				return !!value;
		}
	}
});

RequestSanitizer.addRule('int', {
	filter : function (accept, value , field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

		if (isNaN(value) || parseInt(value) !== value || isNaN(parseInt(value, 10))) {
			return 0;
		}

		return parseInt(value);
	}
});

RequestSanitizer.addRule('float', {
	filter : function (accept, value , field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

		value = parseFloat(value);

		return isNaN(value) ? 0 : value;
	}
});

RequestSanitizer.addRule('object', {
	filter : function (accept, value , field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

	   return Object.prototype.toString.call(value) === '[object Object]' ? value : {};
	}
});

RequestSanitizer.addRule('array', {
	filter: function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

		if (typeof value === 'string' && (typeof accept === 'string' || accept instanceof RegExp )) {
			return value.length ? value.split(accept) : [];
		} else if (Array.isArray(value)) {
			return value;
		} else {
			return [];
		}
	}
});

RequestSanitizer.addRule('arrayOf', {
	validate : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return null; }
		if (! Array.isArray(value)) { return false; }

		var l = value.length;
		var i = -1;
		var child;

		while (++i < l) {
			field.child(i, value[i], accept, value).validate();
		}
	}
});

RequestSanitizer.addRule('hex', {
	filter : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

		return value;
	},
	validate : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return; }

		return /^([A-Fa-f0-9]{2})*$/.test(String(value||''));
	}
});

RequestSanitizer.addRule('buffer', {
	filter : function (accept, value) {
		if (typeof accept !== 'string') {
			accept = 'utf8';
		}

		try {
			return new Buffer(value||'', accept);
		} catch (err) {
			return new Buffer();
		}
	}
});

RequestSanitizer.addRule('variant', {
	filter : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

		return typeof value === 'undefined' ? '' : value;
	}
});

RequestSanitizer.addRule('required', {
	message : 'value is required',
	validate : function (accept, value) {
		return (typeof value !== 'undefined') === accept;
	}
});

RequestSanitizer.addRule('default', {
	filter : function (accept, value) {
		return (typeof value === 'undefined') ? accept : value;
	}
});

RequestSanitizer.addRule('properties', {
	validate : function (accept, value, field) {
		if (! field.isObject()) { return false; }

		Object.getOwnPropertyNames(accept).forEach(function (name) {
			var childAccept = accept[name];
			if (typeof childAccept === 'string') {
				childAccept = convertStringRule(childAccept);
			}
			var child = field.child(name, value[name], childAccept, value);
			child.validate(function (err, report, output) {
				if (err) { throw err; }

				value[name] = output;
			});
		});
	}
});

RequestSanitizer.addRule('minLength', {
	message : 'minimum length is ${accept}.',
	validate : function (accept, value) {
		return value.length >= accept;
	}
});

RequestSanitizer.addRule('case', {
	message : 'case is ${accept}.',
	validate : function (accept, value) {
		return typeof value === 'string' && ((accept==='lower' && value===value.toLowerCase())||(accept==='upper' && value===value.toUpperCase()));
	},
	filter : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return null; }

		if(accept==='lower') {
		  return String(value||'').toLowerCase();
		}
		else if(accept==='upper') {
		  return String(value||'').toUpperCase();
		}
	}
});

RequestSanitizer.addRule('maxLength', {
	message : 'maximum length is ${accept}.',
	validate : function (accept, value) {
		return value.length <= accept;
	}
});

RequestSanitizer.addRule('maxByteLength', {
	message : 'maximum size is ${accept.length} bytes',
	accept : function (accept) {
		if (typeof accept !== 'object') {
			accept = {
				encoding : 'utf8',
				length : accept
			};
		}
		return accept;
	},
	validate : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return; }

		if (typeof value === 'object' && value !== null) {
			value = JSON.stringify(value);
		}


		return Buffer.byteLength(value, 'utf8') <= accept.length;
	}
});

RequestSanitizer.addRule('minByteLength', {
	message : 'minimum size is ${accept.length} bytes',
	accept : function (accept) {
		if (typeof accept !== 'object') {
			accept = {
				encoding : 'utf8',
				length : accept
			};
		}

		return accept;
	},
	validate : function (accept, value, field) {
		if (field.isEmpty() && field.rules.empty) { return; }

		if (typeof value === 'object' && value !== null) {
			value = JSON.stringify(value);
		}
		return Buffer.byteLength(value, 'utf8') >= accept.length;
	}
});

/**
 * Express middleware factory
 * @param {Object} options Validator constructor options
 * @returns {Function} Express middleware
 */
RequestSanitizer.express = function (options) {
	options = extend({}, RequestSanitizer.options, options);


	return function (req, res, next) {
		req.sanitize = sanitize;

		function sanitize(value, properties, callback) {
			var values = {};
			if (typeof value === 'string') {
				value = req[value] || {};
			}

			Object.getOwnPropertyNames(properties).forEach(function (name) {
				values[name] = value.hasOwnProperty(name) ? value[name] : undefined;
				if (typeof properties[name] === 'string') {
					properties[name] = convertStringRule(properties[name]);
				}
			});

			return (new RequestSanitizer(options)).validate(values, {properties:properties}, callback);
		}

		next();
	};
};

// Define filter rules as standalone methods
var rules = RequestSanitizer.prototype.rules;
[
	'string',
	'boolean',
	'int',
	'float',
	'variant',
	'array',
	'object',
	'hex',
	'buffer'
].forEach(function (name) {
	var rule = rules[name];
	if (typeof rule.filter !== 'function') { return; }
	if (name in RequestSanitizer) { return; }

	RequestSanitizer[name] = function filter(value, extra) {
		var rules = {};
		if (typeof extra === 'object') {
			extend(rules, extra);
		} else if (typeof extra !== 'undefined') {
			rules.empty = extra;
		}

		rules[name] = true;

		var report = (new RequestSanitizer(RequestSanitizer.options)).validate(value, rules);
		if (! report.isValid) {
			var error = new Error(report.issues);
			error.name = 'ValidationError';
			error.issues = report.issues;
			throw error;
		}

		return report.value;
	};
});

RequestSanitizer.options.reporter = SanitizeReporter;

function SanitizeReporter(validator) {
	this.validator = validator;
}

//
//__API__ `format`

//
SanitizeReporter.prototype.format = function (message, values) {
	return String(message).replace(/\$\{([^}]+)}/g, function (match, id) {
		return getByPath(values, id.split('.')) || '';
	});
};

//
//__API__ `convert`

//
SanitizeReporter.prototype.convert = function (issues) {
	var self = this;

	var grouped = issues.reduce(function (result, item) {
		var path = item.path.join('.');
		if (path in result === false) { result[path] = []; }

		result[path].push(item);

		return result;
	}, {});

	var result = '';

	Object.getOwnPropertyNames(grouped).forEach(function (path) {
		result += 'Property \'' + path + '\':\n';

		grouped[path].forEach(function (item) {
			var rule = self.validator.getRule(item.rule);

			result += '\t- ';

			if (rule.hasOwnProperty('message')) {
				result += self.format(rule.message, item) + '\n';
			} else {
				result += 'break rule \'' + item.rule + '\'\n';
			}
		});
	});

	return result;
};

function getByPath (target, path) {
	var segment;
	path = path.slice();
	var i = -1;
	var l = path.length - 1;

	while (++i < l) {
		segment = path[i];
		if (typeof target[segment] !== 'object') {
			return null;
		}

		target = target[segment];
	}

	return target[path[l]];
}

function convertStringRule (rule) {
	var result = {};

	if (rule.charAt(rule.length-1) === '!') {
		result.required = true;
		rule = rule.slice(0, -1);
	} else if (rule.charAt(rule.length-1) === '?') {
		result.empty = true;
		rule = rule.slice(0, -1);
	}

	result[rule] = true;
	return result;
}
